/* ************************************************************************ */
/*                                                                          */
/*  haXe Video                                                              */
/*  Copyright (c)2007 Nicolas Cannasse                                      */
/*                                                                          */
/* This library is free software; you can redistribute it and/or            */
/* modify it under the terms of the GNU Lesser General Public               */
/* License as published by the Free Software Foundation; either             */
/* version 2.1 of the License, or (at your option) any later version.	      */
/*                                                                          */
/* This library is distributed in the hope that it will be useful,          */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        */
/* Lesser General Public License or the LICENSE file for more details.      */
/*                                                                          */
/* ************************************************************************ */
package hxvid;
import hxvid.events.IEvent;
import hxvid.events.ServerEvent;
import hxvid.events.ApplicationEvent;
import hxvid.Client;
import hxvid.Data;
//import hxvid.IApplication;
//import neko.vm.Loader;
//import neko.vm.Module;
typedef ApplicationInfo=
{
	var name : String;
	var version : Int;
	var application : Dynamic;
	var config : ApplicationConfig;
	var clients : Array<Client>;
	var sharedStreams : Hash<RtmpStream>;
	var sharedObjects : Hash<SharedObject>;
}
class Server extends RealtimeServer<Client> 
{
	public static var defaultConfig:ServerConfig =
	{
			host : "0.0.0.0",
			port : 1935,
			appsDir : "applications",
			logFile : "log.txt",
			flvBufferTime : 5.0,// 5 seconds of buffering
			clientBufferSize : 1 << 18, // 256 KB buffer
			//----
			listenValue : 10,
			connectLag : 0.05,
			minReadBufferSize : 1 << 10, // 1 KB
			maxReadBufferSize : 1 << 16, // 64 KB
			writeBufferSize : 1 << 18, // 256 KB
			blockingBytes : 1 << 17, // 128 KB
			messageHeaderSize : 1,
			threadsCount : 10
	};
	public var applications:Hash<ApplicationInfo>;
	public var eventHandlers:Hash<Array<Dynamic>>;
	var lock:neko.vm.Mutex;
	public override function new(config:ServerConfig)
	{
		super(config);
		applications = new Hash<ApplicationInfo>();
		eventHandlers = new Hash();
		logger = new Logger(config.logFile);
		logger.id = "Server";
		lock=new neko.vm.Mutex();
	}
	public override function clientConnected( s : neko.net.Socket )
	{
		//dispatchEvent(new ServerEvent(ServerEvent.SOCKET_CONNECT, this, s));
		logger.info("s:"+s.peer().host.toString());
		var c = new Client(this,s);
		//dispatchEvent(new ServerEvent(ServerEvent.CLIENT_CONNECT, this, c));
		return c;
	}
	public override function clientDisconnected( c : Client ) {
		this.dispatchEvent(new ServerEvent(ServerEvent.CLIENT_DISCONNECT, this, c));
		//c.dispatchEvent(new ConnectionEvent(ConnectionEvent.DISCONNECTED, c);
		logger.info("client: "+c.ip);
		c.cleanup();
		removeClient(c);
	}
	public function removeClient(c:Client):Void
	{
		//lock.acquire();
		for(info in applications)
			for(client in info.clients)
				if(client == c)
				{
					info.clients.remove(c);
					break;
				}
		//lock.release();
	}
	public override function logError( e : Dynamic, ?p:haxe.PosInfos ) 
	{
		logger.error(e, p);
	}
	public override function readClientMessage( c : Client, buf : haxe.io.Bytes, pos : Int, len : Int ) {
		var m = c.readProgressive(buf,pos,len);
		if( m == null )
			return null;
		if( m.msg != null )
			c.processPacket(m.msg.header,m.msg.packet);
		return m.bytes;
	}

	public override function clientFillBuffer( c : Client ) {
		c.updateTime(neko.Sys.time());
	}

	public override function clientWakeUp( c : Client ) {
		c.updateTime(neko.Sys.time());
	}

	public static function hexDump(data : String) {
		var sb = new StringBuf();
		for(i in 0...data.length) {
			sb.add(StringTools.hex(data.charCodeAt(i),2));
			sb.add(" ");
		}
		return StringTools.rtrim(sb.toString());
	}
	public function getApplication(client:Client, appName:String):Null<IApplication>
	{
		var appName = appName.split("\\").join("/").split("../").join("");
		var fullPath = neko.Sys.getCwd()+config.appsDir+"/"+appName;
		logger.info("client:"+client.ip+", "+fullPath+"/Application.n");
		
		//application dir not found: return null (this will send NetConnection reject to the calient)
		if(!neko.FileSystem.exists(fullPath))
			return null;
			
		//applicaton is already loaded: return running Application
		if(applications.exists(appName))
		{
			var appInfo = applications.get(appName);
			appInfo.clients.push(client);
			return appInfo.application;
		}
		
		//application dir is found but Application.n is not found: 
		//if isRoom : check if parent directory has Application.n
		//otherwise : return default Application.n
		var appConfig = findApplicationConfig(appName, fullPath);
		if(appConfig.isRoom)
		{
			var top = appName.split("/").shift();
			fullPath = neko.Sys.getCwd()+config.appsDir+"/"+top;
			appConfig = findApplicationConfig(top, fullPath);
		}
		if(!neko.FileSystem.exists(fullPath+"/Application.n"))
		{
			var application:IApplication = new Application(this, appConfig);
			var appInfo = 
			{
				name : appName,
				version:0,
				application : application,
				config : appConfig,
				clients : [client],
				sharedStreams : new Hash<RtmpStream>(),
				sharedObjects : new Hash<SharedObject>()				
			}
			applications.set(appName, appInfo);
			dispatchEvent(new ApplicationEvent(ApplicationEvent.APP_START, this, application));
			return application;
		}
		
		//application dir is found and Application.n is not found: load and return found Application.n
		var nhandle = neko.vm.Loader.local().l;
		var nloader = new neko.vm.Loader(nhandle);
		var nmodule = nloader.loadModule(fullPath+"/Application.n");
        var classes : Dynamic = nmodule.exportsTable().__classes;
		
		var application:IApplication = null;
		try
		{
			application = Type.createInstance(classes.Application, [this, appConfig]); 
		}
		catch(e:Dynamic)
		{
			logger.error(e);
			return null;
		}
		var appInfo=
		{
			name : appName,
			version:0,
			application : application,
			config : appConfig,
			clients : [client], 
			sharedStreams : new Hash<RtmpStream>(),
			sharedObjects : new Hash<SharedObject>()
		}
		applications.set(appName, appInfo);
		dispatchEvent(new ApplicationEvent(ApplicationEvent.APP_START, this, application));
		return application;
	}
	public function reloadApplication(appName:String):Void
	{
		//trace(appName);
		//trace(applications.exists(appName));
		if(!applications.exists(appName))
			return;
		try
		{
			applications.get(appName).application.cleanup();
		}
		catch(e:Dynamic)
		{
			logger.warning(e);
		}
		var fullPath = neko.Sys.getCwd()+config.appsDir+"/"+appName;
		//trace(fullPath);
		//trace(neko.FileSystem.exists(fullPath+"/Application.n"));
		if(!neko.FileSystem.exists(fullPath+"/Application.n"))
			return;

		var appConfig = findApplicationConfig(appName, fullPath);
		var oldInfo = applications.get(appName);
		var version = ++oldInfo.version;
		appConfig.version=version;
		var tempName = "/Application"+version+".n";
		neko.FileSystem.rename(fullPath+"/Application.n", fullPath+tempName);
		
		var nhandle = neko.vm.Loader.local().l;
		var nloader = new neko.vm.Loader(nhandle);
		var nmodule = nloader.loadModule(fullPath+tempName);
        var classes : Dynamic = nmodule.exportsTable().__classes;
		neko.FileSystem.rename(fullPath+tempName, fullPath+"/Application.n");
		var application:IApplication = null;
		try
		{
			application = Type.createInstance(classes.Application, [this, appConfig]); 
		}
		catch(e:Dynamic)
		{
			//trace(e);
			logger.error(e);
			return;
		}
		var appInfo=
		{
			name : appName,
			version: version,
			application : application,
			config : appConfig,
			clients : [], 
			sharedStreams : new Hash<RtmpStream>(),
			sharedObjects : new Hash<SharedObject>()
		}
		logger.debug(fullPath+"/Application.n");
		applications.set(appName, appInfo);
		dispatchEvent(new ApplicationEvent(ApplicationEvent.APP_RELOAD, this, appInfo));
	}
	public function getClients(c:Client):Array<Client>
	{
		for(info in applications)
			for(client in info.clients)
				if(client == c)
					return info.clients;
		return null;
	}
	
	public function getSharedStreams(c:Client):Hash<RtmpStream>//, streamName:String):Hash<RtmpStream>;
	{
		for(info in applications)
			for(client in info.clients)
				if(client == c)
					return info.sharedStreams;//.get(streamName);
		return null;
	}
	public function getSharedObjects(c:Client)//, soName:String):Hash<SharedObject>
	{
		for(info in applications)
			for(client in info.clients)
				if(client == c)
					return info.sharedObjects;//.get(soName);
		return null;
	}
	public function getApplicationName(c:Dynamic):String
	{
		if(Std.is(c,Client))
		{
			for(name in applications.keys())
				for(client in applications.get(name).clients)
					if(client == c)
						return name;
		}
		else
		{
			for(name in applications.keys())
				if(applications.get(name).application==c)
					return name;
		}
		return null;
	}
	public function getApplicationConfig(c:Client):ApplicationConfig
	{
		for(info in applications)
			for(client in info.clients)
				if(client == c)
					return info.config;
		return null;
	}
	function findApplicationConfig(appName:String, fullPath:String,  ?configFile:String="config.xml"):ApplicationConfig
	{
		var config = Application.defaultConfig;
		config.logFile = neko.Sys.getCwd() + this.config.appsDir + "/" + appName+"/" + config.logFile;
		try
		{
			var source = neko.io.File.getContent(fullPath + "/" + configFile);
			var xml = Xml.parse(source).firstElement();
			for(field in Reflect.fields(config))
				if(xml.exists(field))
					Reflect.setField(config, field, switch(field)
					{
						case "streamsDir", "soDir":
							cast xml.get(field);
						case "logFile":
							cast neko.Sys.getCwd() + this.config.appsDir + "/" + appName+"/" + xml.get(field);
						case "isRoom": 
							xml.get(field)=="true" || xml.get(field)=="1";
						default:
							logger.warning("attribute:"+field +" not found in " + configFile +". Using default value:" + Reflect.field(config,field));
							cast Reflect.field(config,field);
					});
		}
		catch(e:Dynamic)
		{
			logger.warning(e);
		}
		config.name = appName;
		return config;
	}
	public function addEventListener(type:String, func:Dynamic)
	{
		if(eventHandlers.exists(type))
		{
			for(f in eventHandlers.get(type))
				if(Reflect.compareMethods(f, func))//if(f == func)
				{
					return;
				}
			eventHandlers.get(type).push(func);
		}
		else
		{
			eventHandlers.set(type,[func]);
		}
	}
	public function removeEventListener(type:String, func:Dynamic)
	{
		if(eventHandlers.exists(type))
		{
			var handlers = eventHandlers.get(type);
			for(i in 0...handlers.length)
				if(Reflect.compareMethods(handlers[i], func))
				{
					handlers.splice(i, 1);
					break;
				}
		}
	}
	
	public function dispatchEvent(event:IEvent):Void
	{
		if(eventHandlers.exists(event.type))
			for(f in eventHandlers.get(event.type))
			{
				try
				{
					f(event);
				}
				catch(e:Dynamic)
				{
					logger.error(event.type+", "+e);
				}
			}
	}
}
